<?php

//include_once("cmsConfig.php");

	if (!defined("CMS_FORMAT"))
		define("CMS_FORMAT","content");
		
	if (!defined("CMS_FORM_DESC_FORMAT"))
		define("CMS_FORM_DESC_FORMAT","formdescriptor");

//	if (CMS_QUERY_DEBUG)
//		echo "CmsDocument Query Debug Mode";
		
	class CmsDocument {

		var $db;
		var $documentTableWriter;
		var $formTableWriter;
		var $document;
		var $formDescriptor;
		var $pageNumber;
		var $pageSize;
		var $constraints;
		var $sorts;
		var $documentCodes;
		var $count;
		var $joins;
		var $joinDirections;
		var $joinTableAliases;
		var $sortJoins;
		var $fetchKeys;
		var $keywords;
		var $sortrank;
		var $siteFilters;
		
		
		/**
		 * @desc This is the default constructor for the CmsDocument. It takes  dbConnection and an optional guid. The Guid provides a way of ensuring that the CmsDocument is unique and provides a way of having various per session caches that are unique to a specific CmsDocument.
		 * @param CmsDatabaseConnection $db
		 */
		function CmsDocument(&$db, $guid=null) {
			
			$this->db = $db;
			$this->documentTableWriter = new CmsTableWriter(CMS_FORMAT, $this->db);
			$this->formDescriptorTableWriter = new CmsTableWriter(CMS_FORM_DESC_FORMAT, $this->db);
			
			$this->setConstraints(array());
			$this->setSorts(array());
			$this->joins = array();
			$this->joinDirection = array();
			$this->keywords = array();
			$this->sortrank = false;
			$this->siteFilters = array();
						
		}
		

		/**
		 * @desc This returns a mapping of keys to fields to be used in queries		 
		 * @return String
		 */
		function getQueryFetchKeys() {
			$fetchKeys = $this->fetchKeys();
			if (!is_null($fetchKeys) && is_array($fetchKeys) && count($fetchKeys) > 0 ) {
				
				if (!key_exists('documentcode',$fetchKeys))
					$fetchKeys[ "documentcode"] = "documentcode";
					
				$keyArray = array();
				foreach ($fetchKeys as $key => $toName) {
					array_push($keyArray, "c." . $key . " AS " . $toName );
				}
			
				$keys = implode(", ", $keyArray);
			} else {
				$keys= "c.*";
			}
			return $keys;
		}
		
		
		/**
		 * @desc This opens the document specified by documentcode, and returns the table rows.
		 * @param integer $documentCode
		 * @return stdclass
		 */		
		function openDocument($documentCode){
			
			$keys = $this->getQueryFetchKeys();

			// keys = new method ..
			$query = "SELECT DISTINCT " . $keys . " " . $this->fromWhereClauseForQuery(false) . " AND c.documentcode='" . $documentCode . "'";
			$res = $this->db->executeQuery( $query );
			$o = $this->db->nextObject();

			//print_r($o);
			if (!$o) {
				$this->document = null;
				return false;
			} else {
				$this->document = $o;
			}
			return $this->document;
		}
		
		/**
		 * @desc pulls the object / content from the db that is at the specified index in the internal document code array
		 *
		 * @param integer $index
		 * @return stdclass
		 */
		function openDocumentAtIndex($index){
			if ($index < 0 || $index >= count($this->documentCodes))
				return false;
				
			$documentCode= $this->documentCodes[ $index ];
			if ($documentCode && $documentCode> 0 && $documentCode < count($this->documentCodes)){
				return $this->openDocument($documentCode);
			}
			return false;
		}
		
		/**
		 * @desc same as openDocumentAtIndex but uses a caching openDocument
		 *
		 * @param integer $index
		 * @return stdclass
		 */
		function openDocumentFromCacheAtIndex($index){
			if ($index < 0 || $index >= count($this->documentCodes))
				return false;
				
			$documentCode= $this->documentCodes[ $index ];
			if ($documentCode && $documentCode> 0){
				return $this->openDocumentFromCache($documentCode->documentcode);
			}
			
			return false;			
		}
		
		/**
		 * @desc This method returns a document with the specified document code, but checks a cache first.. 
		 *
		 * @param integer $documentCode
		 * @return stdclass
		 */
		function openDocumentFromCache($documentCode){
			static $documentCache;//
			$document= false;
			if (null == $documentCache)
				$documentCache = array();
				
			if (key_exists($documentCode, $documentCache)){
				$document = $documentCache[ $documentCode ];
			} else {
				//cache miss.. so we fault..
				
				$document = $this->openDocument($documentCode);
				if (false !== $document){
					$documentCache[ $documentCode ] = $document;
				}
				
			}
			return $document;
		}

		/**
		 * @desc This attempts to look for a document with the specified meta data label
		 *
		 * @param string $label
		 * @return stdclass
		 */
		function openDocumentWithLabel($label){
			$fetchKeys = $this->fetchKeys();
			if (!is_null($fetchKeys) && is_array($fetchKeys) && count($fetchKeys) > 0 ) {
				
				if (!key_exists('documentcode',$fetchKeys))
					$fetchKeys[ "documentcode"] = "documentcode";
					
				$keyArray = array();
				foreach ($fetchKeys as $key => $toName) {
					array_push($keyArray, "c." . $key . " AS " . $toName );
				}
			
				$keys = implode(", ", $keyArray);
			} else {
				$keys= "c.*";
			}
			$this->sortrank = true;
			$query = "SELECT DISTINCT " . $keys . " " . $this->fromWhereClauseForQuery(false) . " AND d.label='" . $label . "'";
			$res = $this->db->executeQuery( $query );
			$o = $this->db->nextObject();

			//print_r($o);
			if (!$o) {
				$this->document = null;
				return false;
			} else {
				$this->document = $o;
			}
			return $this->document;
		}

		/**
		 * @desc flush resets the CmsDocument, clearing out any cached values and removes all constraints, joins and sorts.
		 *
		 */
		function flush(){
			$this->removeAllConstraints();
			$this->removeAllSorts();
			$this->removeAllJoins();
			$this->removeAllKeywords();
			$this->removeAllJoinDirections();
			$this->joinTableAliases = null;
			$this->document = null;
			$this->documentCodes = null;
			$this->count = null;
			$this->removeAllSortJoins();
			$this->clearFetchKeys();
			//add site filters clear here
			$this->removeAllSiteFilters();
		}
		
		/**
		 * @desc This resets the iterator to the beginning of the requested list of documents, this is typically called after the CmsDocuments joins, sorts and constraints are set.
		 *
		 */
		function resetIterator() {							
			$query = $this->documentIteratorQuery();
			//echo $query;
			$this->documentCodes = $this->db->getAllAsObjects($query, ($n = null) );
		}
		
		/**
		 * @desc resets the internal document code list to the beginning, saves having to reset the iterator if the query has not changed
		 *
		 */
		function resetInternalDocumentCodeList(){
			reset($this->documentCodes);
		}
		
		/**
		 * @desc This returns an SQL query that is used by the iterator and bound by the CmsDocuments constraints, joins, current page size and number. It is also sorted by the cms documents orderings.
		 * 
		 * @return string
		 */
		function documentIteratorQuery(){
			$score = "100 ";
			$groupby = "";
			if (is_array($this->keywords) && count($this->keywords) > 0 ){
				$score = " COUNT(skwd.word) ";
				$groupby = " GROUP BY skwd.documentcode ";	
			}
			return $query = "SELECT DISTINCT c.documentcode AS documentcode, " . $score . " AS score " . $this->fromWhereClauseForQuery() . $groupby . $this->orderClauseForQuery() . $this->limitClauseForQuery();
		}
		
		/**
		 * @desc  returns an SQL query that is used to count all documents that match the configured constraints and joins. It is not bound by page size or number. As it is a count, it is not sorted.
		 *
		 * @return string
		 */
		function countQuery() {
			return $query = "SELECT COUNT(DISTINCT c.documentcode) AS count" . $this->fromWhereClauseForQuery() ;//. $this->orderClauseForQuery();
		}
		

		/**
		 * @desc This returns a query fragment that can provide the FROM <tables> WHERE <constraints> part of a query used by the CmsDocument. This takes into consideration user permissions and groups if required to.
		 *
		 * @param boolean $includeConstraints
		 * @return string
		 */
		function setKeyWords($keywords){
			$keywords = keyWordReduce($keywords);
			if (!is_array($keywords))
				$this->keywords = array();
			$this->keywords = array_merge($this->keywords, $keywords);	
		}
		
		/**
		 * @desc this clears the search keywords array
		 *
		 */
		function removeAllKeywords() {
			$this->keywords = array();
		}

		/**
		 * @desc Generates the from where part of the query used to get content
		 *
		 * @param unknown_type $includeConstraints
		 * @return string
		 */
        function fromWhereClauseForQuery($includeConstraints = true) {
        
                        global $authInfo;
                        
                        $query =" ";
                        //print_r($authInfo);
                        //do the permission mangle dangle
                        if (CMS_ENABLE_USER_PERMISSIONS && $authInfo->usertype != CMS_USERTYPE_SU) {
                        
                                $gids = groupsForUserId($authInfo->uid, $this->db);
                                $gidStack=array();
                                foreach ($gids as $key => $gid) {
                                        array_push($gidStack, $gid);
                                }
                                $gidList= implode(",", $gidStack);
                                if (strlen($gidList) <= 0 ) {
                                        $gidList = "NULL";
                                }
                                $query .= 
                                "AND " . 
                                //this is the block for reading the content
                                "((c.permissions > 0 AND (  ( c.permissions & " .  (PERMISSION_VIEW << PERMISSION_ALL) . " ) | (c.gid IN ( ". $gidList . " ) AND (c.permissions & " .  (PERMISSION_VIEW << PERMISSION_GROUP) . ") ) | (c.uid = " . $authInfo->uid . " AND ( c.permissions & " .  (PERMISSION_VIEW << PERMISSION_USER) . " )))) "
                                . 
                                "OR" . 
                                // this is the block for falling back to the form
                                " ((c.permissions = 0 OR c.permissions IS NULL) AND (  ( f.permission & " .  (PERMISSION_VIEW << PERMISSION_ALL) . " ) | (f.gid IN ( " . $gidList ." ) AND (f.permission & " .  (PERMISSION_VIEW << PERMISSION_GROUP) . ") ) | (f.uid = " . $authInfo->uid . " AND ( f.permission & " .  (PERMISSION_VIEW << PERMISSION_USER) . " ))))"
                                
                                .
                                ")" 
                                ;
                        }

                        $joinTableAliases="";
                        if ($includeConstraints) {
                        
							// add the keywords..
							if (is_array($this->keywords) && count($this->keywords) > 0 ) {
					
								$joinTableAliases .=", searchkeyword AS skwd ";
								$query .= " AND ( c.documentcode = skwd.documentcode AND skwd.word IN ('" . implode("','", $this->keywords) . "') ) ";
								$preQuery = " ,count(skwd.word) AS score, skwd.documentcode ";
							}
					    
							$tableJoins = $this->serialiseTableJoins();
							if (count($this->joinTableAliases) > 0 ) {
								
								$tableAliasJoinStack = array();
									
								foreach( $this->joinTableAliases AS $key=> $value) {
									array_push($tableAliasJoinStack, ", content AS c".$value .", manytomanydata AS mm".$value);
								}
								$joinTableAliases = implode("", $tableAliasJoinStack);
							}

							$joinConstraintStack = array();
							foreach($this->joins as $key => $value){
								$tableAlias = $this->joinTableAliases[ $key ];
								array_push($joinConstraintStack, $this->serialiseConstraintsFromArrayWithTableAlias($value, "c".$tableAlias));
							}
							
							if (count($joinConstraintStack) > 0) {
								$joinConstraints = " (" . implode(") AND (", $joinConstraintStack) . ") ";
							}
							
							if (isset($joinConstraints) && is_string($joinConstraints) && strlen($joinConstraints) > 0 ){
								$query .= " AND " .$tableJoins. " AND " .$joinConstraints;
							} else {
								$query .= " ";
							}
                            

                            $constraints = $this->serialiseConstraints();                            
                            if (is_string($constraints) && strlen($constraints) > 0){
                                $query .= " AND " . $constraints;
                            } else {
                                $query .= " ";
                            }            
                            
                            //add in the sort table joins if there are any
	                        if (count($this->sortJoins) > 0 )
	                        	$joinTableAliases .= $this->serialiseSortTableJoins();
	                        	
	                        //add in the sort table join constraints if there are any
	                        if (count($this->sortJoins) > 0 )
	                        	$query .= $this->serialiseSortJoinConstraints();
	                        	
							//add the site filter here.
							if ( 0 < count($this->getSiteFilters()) ){
								//
								/* where documenttypecode = () and (sitemode thing in () )*/
								$query .= " AND ( c.documentcode=sc.documentcode AND (";
												
								//now the sites... 
								$query .= "sc.sitegroupid IN (" . implode( " , " , $this->getSiteFilters() );
										
								$query .=")))";
								
							}
								
                        }
                        	
                        $q = " FROM  content AS c, formdescriptor AS f ";
						if ($this->sortrank) {
							$q .= ", cmscontentmetadata AS d ";
						}
						
						if($includeConstraints && 0 < count($this->getSiteFilters())){
							$q .= ", sitecontent AS sc ";
						}
						
						$q .= $joinTableAliases . 
                        " WHERE f.formid = c.formid ";
						if ($this->sortrank) {
							$q .= " AND d.documentcode = c.documentcode ";
						}
						$q .= $query;
                        
                        //echo $q;
                        
                return $q;                
        }
        
        
                
        /**
         * @desc This produces the ORDER BY part of the sql query used by iterators.
         *
         * @return string
         */
		function orderClauseForQuery(){
			$order = " ";
			$sorts = $this->serialiseSorts();
			if (is_string($sorts) && strlen($sorts) > 0){
				$order .= " ORDER BY " . $sorts;
			}
			return $order . " ";
		}
		
		/**
		 * @desc This produces the limit clause for queries. This used to restrict the  the result set to the specified page size and number.
		 *
		 * @return string
		 */	
		function limitClauseForQuery() { 
			return " LIMIT " . ($this->pageNumber() * $this->pageSize() ) . "," . $this->pageSize();
		}
		
		/**
		 * @desc this returns the next document in the iterator and shifts the result set along one record.
		 *
		 * @return stdclass
		 */
		function nextDocument() {
			$o = $this->openCurrentDocument();
			if (!is_null($this->documentCodes) && is_array($this->documentCodes)) {
				next($this->documentCodes);
				return $o;
			}
		}
		
		/**
		 * @desc this reads the next document in the document code array, but uses the caching methods hopefully avoiding a round trip to the database if the document has been read already
		 *
		 * @return unknown
		 */
		function nextDocumentFromCache(){
			$o = $this->openCurrentDocumentFromCache();
			if (!is_null($this->documentCodes) && is_array($this->documentCodes)) {
				next($this->documentCodes);
				return $o;
			}
		}
		
		/**
		 * @desc reads the current document in the document code array, but checks a cache first.
		 *
		 * @return stdclass
		 */
		function openCurrentDocumentFromCache(){
			if (!is_null($this->documentCodes)) {
				$o = current($this->documentCodes);
				if ($o->documentcode && is_numeric($o->documentcode))
					return $this->openDocumentFromCache($o->documentcode);
				else 
					return false;			
			}
		}
		
		/**
		 * @desc This returns an array of all documents returned after the resetIterator call
		 * dont do this on big data sets unless you are <<simple>> as you
		 * will more than likely a) run out of memory, b) not work, c) never finish.
		 *
		 * @return array
		 */
		function getAllDocuments(){
			// we are assuming here that the document iterator has already been reset.
			
			$keys = $this->getQueryFetchKeys();
			
			//since we are getting all.. documents that we are allowed to get.. we need a slightly different query..
			$documentCodes = array();
			foreach ($this->documentCodes as $documentCode){
				array_unshift($documentCodes, $documentCode->documentcode);
			}
			$query = "SELECT DISTINCT " . $keys . " " . $this->fromWhereClauseForQuery(false) . " AND c.documentcode IN (" . implode(",",$documentCodes) . ")" . " " . $this->orderClauseForQuery();

			$res = $this->db->getAllAsObjects($query, ($n = null));
			
			if (!$res) {
				return false;
			} 
			return $res;
		}
		
		
		
		/**
		 * @desc this returns the current document in the iterator but does not shift  the record.
		 *
		 * @return stdclass
		 */
		function nextResultRecord() {
			$record = current($this->documentCodes);
			next($this->documentCodes);
			return $record;
		}
		
		/**
		 * @desc reads the current doucment in the internal documentcodes array
		 *
		 * @return string
		 */
		function openCurrentDocument(){

			if (!is_null($this->documentCodes)) {
				$o = current($this->documentCodes);
				if ($o->documentcode && is_numeric($o->documentcode))
					return $this->openDocument($o->documentcode);
				else 
					return false;			
			}
		}
		
		/**
		 * @desc Returns the current page number of the result set
		 *
		 * @return integer
		 */
		function pageNumber(){
			if (!$this->pageNumber)
				return 0;
			else 
				return $this->pageNumber;
		}
		
		/**
		 * @desc This sets the page number of the result set that we want to view with the iterator.
		 *
		 * @param integer $pageNumber
		 */
		function setPageNumber($pageNumber) {
			$this->pageNumber = $pageNumber;
		}
		
		/**
		 * @desc finds the current page offset.. or the position in the arry of the current page..
		 *
		 * @return integer
		 */
		function pageOffset(){
			$totalPages = $this->totalPages();
			
			if ($totalPages === 1) {
				return 0;
			} else if ($this->pageNumber() >= $totalPages) {
				$offset =  ($totalPages - 1)* $this->pageSize();
			} else {
				$offset = $this->pageNumber() * $this->pageSize();
			}
			return $offset;
		}

		/**
		 * @desc returns the total number of pages available
		 *
		 * @return integer
		 */
		function totalPages(){
			$pageSize = (int) $this->pageSize();
			$count = $this->totalDocuments();
			
			//if zero documents there will always be one (empty) page so return one
			if($count > 0) {
				$totalPages = (int) (  $count / $pageSize );
				$totalPagesRemainder = (int)$count % (int)$pageSize;
				if ($totalPagesRemainder > 0) {
					$totalPages += 1;
				}
			} else {
				$totalPages = 1;
			}
			return $totalPages;
		}	
		
		/**
		 * @desc returns the count of objects on the current page
		 *
		 * @return integer
		 */
		function totalDocumentsOnPage() {
			return count($this->documentCodes);
		}
		
		/**
		 * @desc sets the page site
		 *
		 * @param integer $pageSize
		 */
		function setPageSize($pageSize) {
			$this->pageSize = $pageSize;
		}
				
		/**
		 * @desc returns the current page size
		 *
		 * @return integer
		 */
		function pageSize(){
			if (!$this->pageSize)
				return $this->totalDocuments();
				
			return $this->pageSize;
		}
		
		/**
		 * @desc returns the total documents found for reading
		 *
		 * @return integer
		 */
		function totalDocuments() {
			$query = $this->countQuery();
			//echo $query;
			$this->db->executeQuery( $query );
			$o = $this->db->nextObject();
			return  $o->count;
		}
//--


//-- RELEATIONS


	/**
	 * @desc adds a join constraint to another document type
	 *
	 * @param string $documentType
	 * @param string $key
	 * @param string $value
	 * @param string $type
	 */
    function addJoinConstraint($documentType, $key, $value, $type="=") {
    
        
       if ($this->joins ==null)
            $this->joins = array();
           
        if (key_exists($documentType, $this->joins) && $this->joins[ $documentType ] == null )
            $this->joins[ $documentType ] = array();
               
        $this->addConstraintToArray( $this->joins[ $documentType ], $key, $value, $type);

    }
    
	/**
	 * @desc Sets the direction of the specified join constraint
	 *
	 * @param unknown_type $documentType
	 * @param unknown_type $direction
	 */
    function setJoinDirectionForJoin($documentType, $direction="PARENT-TO-CHILD") {
        if ( key_exists($documentType, $this->joins) && $this->joinDirections == null)
            $this->joinDirections = array();
            
            $this->joinDirections[ $documentType ] = $direction;
            
    }
    
    /**
     * @desc removes all join directions
     *
     */
    function removeAllJoinDirections(){
        $this->joinDirection = array();   
    }
    
    /**
     * @desc removes all joins, should be used in conjunction with removeAllJoinDirections
     *
     */
    function removeAllJoins(){
        $this->joins = array();
    }
    
    /**
     * adds a sort join to allow for sorting on a column on an external documenttype
     *
     * @param string $parentDocumentField
     * @param string $sortField
     * @param string $direction
     * @param boolean $castToInt
     */
    
    function addSortJoin($parentDocumentField, $sortField, $direction = "ASC", $castToInt = false) {
    	
    	//Generate a list of how many times the content table needs to be joined.
    	if ($this->sortJoins==null)
    		$this->sortJoins = array();

    	if(key_exists($parentDocumentField, $this->sortJoins) && !is_array($this->sortJoins[$parentDocumentField])) {
    		$this->sortJoins[$parentDocumentField]	= array();
    	}
    	$this->sortJoins[$parentDocumentField][$sortField] = $direction;
     	
    	//need to add sort
    	//loop through to work out the next $i
    	$i = 0;
    	foreach($this->sortJoins as $key => $value) {
    		if($key == $parentDocumentField)
    			$x = $i;
    		$i++;
    	}
    	$sortJoinsAliasPostFix = "csj" . $x;
    	if ($castToInt) {
    		$this->addSort(" CAST(".$sortJoinsAliasPostFix.".".$sortField." AS UNSIGNED) ", $direction, true);    		
    	} else {
    		$this->addSort($sortJoinsAliasPostFix.".".$sortField, $direction, true);
    	}
    }
    
    /**
     * @desc removes all sort joins.
     *
     */
    function removeAllSortJoins() {
    	$this->sortJoins = array();
    }
    
  
    
//-- CONSTRAINTS

		// 
		/**
		 * @desc removes any site constraints
		 *
		 */
		function removeAllSiteFilters() {
			$this->siteFilters = array();
		}
		
		/**
		 * @desc adds a site constraint, this takes a site id.
		 *
		 * @param unknown_type $site
		 */
		function addSiteToSiteFilters($site){
			array_push($this->siteFilters, $site);
		}

		/**
		 * @desc this returns the array of specified site constraints/filters
		 *
		 * @return array
		 */
		function getSiteFilters(){
			return $this->siteFilters;
		}		
		
		/**
		 * @desc sets the site filters array
		 *
		 * @param array $siteFilters
		 */
		function setSiteFilters($siteFilters){
			if(is_array($siteFilters)){
				$this->siteFilters = $siteFilters;
			}
		}
		
		/**
		 * @desc sets the internal constraints array
		 *
		 * @param unknown_type $constraints
		 */
		function setConstraints($constraints){
			if (is_array($constraints)) {
				$this->constraints = $constraints;
			}
		}
		
		/**
		 * @desc returns the internal constraints array
		 *
		 * @return unknown
		 */
		function constraints(){
			return $this->constraints;
		}
		
		/**
		 * @desc adds a constraint to the internal constraints array
		 *
		 * @param string $key
		 * @param string $value
		 * @param string $type
		 */
		function addConstraint($key, $value, $type="="){
            $this->addConstraintToArray( $this->constraints, $key, $value, $type);
        }
        
        /**
         * @desc adds a constraint to an array.. builds the constraint structure
         *
         * @param array $array
         * @param string $key
         * @param string $value
         * @param string $type
         */
        
		function addConstraintToArray( &$array, $key, $value, $type="="){
		
		//a key pair 
			$type = strtoupper($type);
			switch ($type){
				case "=":
				case "!=":
				case "LIKE":
				case "IS":
				case "<=":
				case ">=":
				case ">":
				case "<":
				case " IN ":
				case " NOT IN ":				
				case " BETWEEN ":
				
					if (! is_array($array)){
						$array	=array();
					}
					$ev = null;
					if (!(isset($array[$key]) )){
						$ev = array();
					} 
					else {
						$ev = $array[$key];
					}
					if (is_numeric($value))
					$value = "<<" . $value . ">>";
					$ev[$value] = $type;
					$array[$key] = $ev;
				break;
			}
		}
		
		/**
		 * @desc removes the constraints for the specified key
		 *
		 * @param string $key
		 */
		function removeAllConstraintsForKey($key){
			unset( $this->constraints[$key] ); 
		}
		
		/**
		 * @desc removes all constraints..
		 *
		 */
		function removeAllConstraints(){
			$this->constraints = array();
		}
		
		/**
		 * @desc removes a specific constraint
		 *
		 * @param unknown_type $key
		 * @param unknown_type $value
		 */
		function removeConstraintForKeyAndValue($key, $value){
		
			//find the key..
			if (key_exists($key, $this->constraints)){
				$node = &$this->constraints[$key];
				foreach($node as $nodeKey => $nodeValue) {
					//echo "$nodeKey $nodeValue\n";
					if ( $nodeValue === $value) {
						unset( $node[$nodeKey] );
					}
				}		
			}
		}
		
		/**
		 * @desc short cut for adding a document type constraint
		 *
		 * @param string $documentType
		 */
		function addDocumentType($documentType){
			$this->addConstraint("documenttypecode", $documentType);
		}
		
		/**
		 * @desc short cut for adding multuple document types
		 *
		 * @param array $documentTypes
		 */
		function setDocumentTypes($documentTypes){
			if (is_array($documentTypes)){		
				foreach ($documentTypes as $documentType) {
					$this->addDocumentType($documentType);
				}
			}
		}
		
		/**
		 * @desc short cut for adding a constraint for active documents.
		 *
		 * @param boolean $activeOnly
		 */
		function setActiveDocuments( $activeOnly = true ) {
			if ($activeOnly) {
				$this->addConstraint('active', 1, "=");
			} else {
				$this->removeAllConstraintsForKey('active');
			}
		}
		
		/**
		 * @desc allows for the specification of a mapping from internal keys to external keys
		 *
		 * @param array $fetchKeys
		 */
		function setFetchKeys($fetchKeys){
			$this->fetchKeys = $fetchKeys;
		}
		
		/**
		 * @desc returns the mapping from internal to external keys
		 *
		 * @return array
		 */
		function fetchKeys() {
			if (is_null($this->fetchKeys)) return array();
			return $this->fetchKeys;	
		}
		
		/**
		 * @desc clears the key mappings
		 *
		 */
		function clearFetchKeys(){
			$this->setFetchKeys( array());
		}
		
		/**
		 * @desc removes a documenttype constraint
		 *
		 * @param unknown_type $documentType
		 */
		function removeDocumentType($documentType){
			$this->removeConstraintForKeyAndValue("documenttypecode", $documentType);
		}
		
		/**
		 * @desc shortcut for adding formid constraint
		 *
		 * @param unknown_type $fromId
		 */
		function addFormId( $fromId ) {
			$this->addConstraint("formid", $fromId );
		}
		
		/**
		 * @desc shortcut for adding multiple formid constraints
		 *
		 * @param unknown_type $formIds
		 */
		function setFormIds($formIds) {
			if (is_array($formIds)){		
				foreach ($formIds as $formId) {
					$this->addFormId($formId);
				}
			}
		}

		/**
		 * @desc removes gthe specified form id from the constraints..
		 *
		 * @param unknown_type $formId
		 */
		function removeFormId($formId){
			$this->removeConstraintForKeyAndValue("formid", $formId);
		}		
				
//-- SORTS
		/**
		 * @desc returns ths sorts array
		 *
		 * @return unknown
		 */
		function sorts() {
			return $this->sorts;
		}
		
		/**
		 * @desc allows for the setting of the sorts array
		 *
		 * @param unknown_type $sorts
		 */
		function setSorts($sorts){
			if (is_array($sorts)){
				$this->sorts = $sorts;
			}
		}
		
		/**
		 * @desc adds a sort
		 *
		 * @param strimg $column
		 * @param strimg $direction
		 * @param strimg $sortJoin
		 */
		function addSort($column, $direction = "ASC", $sortJoin = false){
		
//			if (CMS_QUERY_DEBUG) 
//				echo "adding a sort on column: " . $column;
				
			$direction = strtoupper($direction);

			switch ($direction){
				case "ASC":
				case "DESC":
					if (!is_array($this->sorts)){
						$this->sorts = array();
					}
					if(!$sortJoin) {
						if (preg_match('/^<<(.*)>>$/', $column, $matches)) {
							if ($matches[1] == "rank") {
								$column = "d.".$matches[1];
								$this->sortrank = true;
							}
							else {
								$column = $matches[1];
							}
						}
						else {
							$column = "c.".$column;
						}
					}
					$this->sorts[$column] = $direction;
				break;
				default:
//					if (CMS_QUERY_DEBUG) 
//						echo "futile attempt to add a sort on column: " . $column . " with direction: " . $direction;
				break;
				// do nothing
			}
		}
		
		/**
		 * @desc removes a sort
		 *
		 * @param string $column
		 */
		function removeSort($column){
			if(key_exists($column, $this->sorts)){
				unset($this->sorts[$column]);
			}
		}
		
		/**
		 * @desc clears all sorts
		 *
		 */
		function removeAllSorts(){
			$this->sorts = array();
		}
		
		
// QUERY Serialisers
		
/**
 * @desc serialises sort joins for use in a query
 *
 * @return string
 */
		function serialiseSortTableJoins() {
			$i = 0;
			$sortJoinTableStack = array();
			foreach($this->sortJoins as $key => $value) {
				$sortJoinsAliasPostFix = "sj" . $i;
				array_push($sortJoinTableStack, ", content AS c".$sortJoinsAliasPostFix);
				$i++;
			}
			$sortJoinTableString = implode("", $sortJoinTableStack);
			return $sortJoinTableString;
		}
		
		/**
		 * @serialises the sort join constraints for use in a query
		 *
		 * @return string
		 */
		function serialiseSortJoinConstraints() {
			$i = 0;
			$sortJoinConstraintStack = array();
			foreach($this->sortJoins as $key => $value) {
				$sortJoinsAliasPostFix = "sj" . $i;
				array_push($sortJoinConstraintStack, " (c.$key = c".$sortJoinsAliasPostFix.".documentcode ) ");
				$i++;
			}
			
			$sortJoinConstraintString = implode(" AND ", $sortJoinConstraintStack);
			if(strlen($sortJoinConstraintString) > 0 )
				$sortJoinConstraintString = " AND ".$sortJoinConstraintString." ";
			return $sortJoinConstraintString;
		}
		
		/**
		 * @desc serialises the table to join on
		 *
		 * @return string
		 */
        function serialiseTableJoins() {
            $this->joinTableAliases = array();
            $i = 0;
            $joinStack = array();

            foreach ($this->joins as $key => $value) {
                $joinAliasPostFix = "j" . $i;
                $this->joinTableAliases[ $key ] = $joinAliasPostFix;
                $joinDirection = $this->joinDirections[ $key ];
                if ( $joinDirection == "CHILD-TO-PARENT") {
                    $col2 = "parentcode";
                    $col1 = "relatedcode";
                } else {
                    $col1 = "parentcode";
                    $col2 = "relatedcode";
                }
                array_push($joinStack, "(c.documentcode = mm" . $joinAliasPostFix . ".".$col1. " AND mm". $joinAliasPostFix.".".$col2." = c". $joinAliasPostFix .".documentcode )");
                $i++;
            }
            $joinString =  implode(") AND (", $joinStack);
            return " (" .$joinString . ") ";
        }
        
        /**
         * @desc serialises the constraints for use in queries
         *
         * @return string
         */
        function serialiseConstraints() {
            return $this->serialiseConstraintsFromArrayWithTableAlias($this->constraints, "c");
        }
        
        /**
         * @serialises an array in to sort of sql stuff
         *
         * @param array $array
         * @param string $alias
         * @return string
         */
		function serialiseConstraintsFromArrayWithTableAlias(&$array, $alias){
			$andStack = array();
			
			foreach ($array as $key => $andValue ) {
				//key mangle..
				$keyMatches = array();
				if (preg_match('/^<<(.*)>>$/', $key, $keyMatches)) {
					$key = $keyMatches[1];
				} else {
					$key = $alias ."." .  $key;
				}
				if (is_array($andValue)) {
					$orStack = array();
					foreach ($andValue as $orValue => $orConstraintType) {
						switch ($orConstraintType) {
							case " IN ":
							case " NOT IN ":
								array_push($orStack, "$key$orConstraintType$orValue");
							break;
							default :
								$matches = array();
								if (preg_match('/^<<(.*)>>$/', $orValue, $matches)) {
									array_push($orStack, "$key $orConstraintType ". $matches[1]);								
								} else {
									array_push($orStack, "$key $orConstraintType \"$orValue\"");
								}

						}
					}
					$orString = implode(" OR ", $orStack);
					if (strlen($orString) > 0 ) {
						array_push($andStack, "(" . $orString . ")" );
					}
				} else {
				    die("should not really ever get here, this is a sigularity");
				}
			}
			$andString = implode(" AND ", $andStack);
			if (strlen($andString) > 0 ){
				return "(" . $andString .")";
			}
			return null;
		}
		/**
		 * @desc serialises the standard sorts
		 *
		 * @return unknown
		 */
		function serialiseSorts(){
			$sortStack = array();
			foreach ($this->sorts() as $column => $direction){
				array_push($sortStack, $column . " " . $direction);
			}
			$weightOrder ="";
			if (is_array($this->keywords) && count($this->keywords) > 0) {
				$weightOrder = " score DESC ";
				if (count($sortStack))
					$weightOrder .= ",";
			}
			return $weightOrder . implode(", ", $sortStack);
		}
		
		
	}

?>
